<script lang="ts" setup>
import { PropType } from 'vue';
import { isNumber } from '@/utils/is';
import { propTypes } from '@/utils/propTypes';
import { useDesign } from '@/hooks/web/useDesign';

defineOptions({ name: 'CountTo' });

const { getPrefixCls } = useDesign();

const prefixCls = getPrefixCls('count-to');

const props = defineProps({
  startVal: propTypes.number.def(0), // 开始播放值
  endVal: propTypes.number.def(2021), // 最终值
  duration: propTypes.number.def(3000), // 动画时长
  autoplay: propTypes.bool.def(true), // 是否自动播放动画, 默认播放
  decimals: propTypes.number.validate((value: number) => value >= 0).def(0), // 显示的小数位数, 默认不显示小数
  decimal: propTypes.string.def('.'), // 小数分隔符号, 默认为点
  separator: propTypes.string.def(','), // 数字每三位的分隔符, 默认为逗号
  prefix: propTypes.string.def(''), // 前缀, 数值前面显示的内容
  suffix: propTypes.string.def(''), // 后缀, 数值后面显示的内容
  useEasing: propTypes.bool.def(true), // 是否使用缓动效果, 默认启用
  easingFn: {
    type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
    default(t: number, b: number, c: number, d: number) {
      return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
    } // 缓动函数
  }
});

const emit = defineEmits(['mounted', 'callback']);

const formatNumber = (num: number | string) => {
  const { decimals, decimal, separator, suffix, prefix } = props;
  num = Number(num).toFixed(decimals);
  num += '';
  const x = num.split('.');
  let x1 = x[0];
  const x2 = x.length > 1 ? decimal + x[1] : '';
  const rgx = /(\d+)(\d{3})/;
  if (separator && !isNumber(separator)) {
    while (rgx.test(x1)) {
      x1 = x1.replace(rgx, '$1' + separator + '$2');
    }
  }
  return prefix + x1 + x2 + suffix;
};

const state = reactive<{
  localStartVal: number;
  printVal: number | null;
  displayValue: string;
  paused: boolean;
  localDuration: number | null;
  startTime: number | null;
  timestamp: number | null;
  rAF: any;
  remaining: number | null;
}>({
  localStartVal: props.startVal,
  displayValue: formatNumber(props.startVal),
  printVal: null,
  paused: false,
  localDuration: props.duration,
  startTime: null,
  timestamp: null,
  remaining: null,
  rAF: null
});

const displayValue = toRef(state, 'displayValue');

onMounted(() => {
  if (props.autoplay) {
    start();
  }
  emit('mounted');
});

const getCountDown = computed(() => {
  return props.startVal > props.endVal;
});

watch([() => props.startVal, () => props.endVal], () => {
  if (props.autoplay) {
    start();
  }
});

const start = () => {
  const { startVal, duration } = props;
  state.localStartVal = startVal;
  state.startTime = null;
  state.localDuration = duration;
  state.paused = false;
  state.rAF = requestAnimationFrame(count);
};

const pauseResume = () => {
  if (state.paused) {
    resume();
    state.paused = false;
  } else {
    pause();
    state.paused = true;
  }
};

const pause = () => {
  cancelAnimationFrame(state.rAF);
};

const resume = () => {
  state.startTime = null;
  state.localDuration = +(state.remaining as number);
  state.localStartVal = +(state.printVal as number);
  requestAnimationFrame(count);
};

const reset = () => {
  state.startTime = null;
  cancelAnimationFrame(state.rAF);
  state.displayValue = formatNumber(props.startVal);
};

const count = (timestamp: number) => {
  const { useEasing, easingFn, endVal } = props;
  if (!state.startTime) state.startTime = timestamp;
  state.timestamp = timestamp;
  const progress = timestamp - state.startTime;
  state.remaining = (state.localDuration as number) - progress;
  if (useEasing) {
    if (unref(getCountDown)) {
      state.printVal =
        state.localStartVal -
        easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number);
    } else {
      state.printVal = easingFn(
        progress,
        state.localStartVal,
        endVal - state.localStartVal,
        state.localDuration as number
      );
    }
  } else {
    if (unref(getCountDown)) {
      state.printVal =
        state.localStartVal -
        (state.localStartVal - endVal) * (progress / (state.localDuration as number));
    } else {
      state.printVal =
        state.localStartVal +
        (endVal - state.localStartVal) * (progress / (state.localDuration as number));
    }
  }
  if (unref(getCountDown)) {
    state.printVal = state.printVal < endVal ? endVal : state.printVal;
  } else {
    state.printVal = state.printVal > endVal ? endVal : state.printVal;
  }
  state.displayValue = formatNumber(state.printVal!);
  if (progress < (state.localDuration as number)) {
    state.rAF = requestAnimationFrame(count);
  } else {
    emit('callback');
  }
};

defineExpose({
  pauseResume,
  reset,
  start,
  pause
});
</script>

<template>
  <span :class="prefixCls">
    {{ displayValue }}
  </span>
</template>
